// 文件/数据库交互模块，直接跟文件与数据库进行数据交互的模块，对文件和数据库进行增删改查，其下有四个子模块：
// 1.（查）题目信息提取模块：提取文件或数据库中的题目信息，并将其返回给调用者。
//以下为后续计划增加的管理员录题功能：
// 2.（增）增加题目模块：往文件或数据库中增加题目信息。
// 3.（删）删除题目模块：删除文件或数据库中的题目信息。
// 4.（改）修改题目模块：修改文件或数据库中的题目信息。
// 文件版本
#pragma once

#include <iostream>
#include <string>        //std::string
#include <vector>        //std::vector
#include <unordered_map> //std::unordered_map
#include <fstream>       //std::ifstream
#include <cstdlib>       //atoi()
#include <cassert>       //assert()

#include "../common/util.hpp" //引入common目录下的工具模块，StringUtil
#include "../common/log.hpp"  //引入common目录下的工日志模块

namespace ns_model
{
    using namespace std;
    using namespace ns_util; // 展开工具模块
    using namespace ns_log;  // 展开日志模块

    // Question结构体：用来描述一个题目，里面保存着一个题目必要的信息
    struct Question
    {
        std::string number; // 题目编号，唯一
        std::string title;  // 题目的标题
        std::string star;   // 难度: 简单 中等 困难
        int cpu_limit;      // 题目的时间要求(S)
        int mem_limit;      // 题目的空间要去(KB)
        std::string desc;   // 题目的描述
        std::string header; // 题目预设给用户在线编辑器的代码
        std::string tail;   // 题目的测试用例，需要和header拼接，形成完整代码
    };

    const std::string questins_list = "./questions/questions.list";
    const std::string questins_path = "./questions/";

    class Model
    {
    private:
        //题库无序图：保存 题号:题目信息 的键值对。（保存所有题目的信息）
        unordered_map<string, Question> questions;

    public:
        Model()
        {
            //读取所有题目文件，将题目信息加载到内存中
            assert(LoadQuestionList(questins_list));
        }
        ~Model()
        {
        }

        /********************************************************************************************************
         * 读取所有题目文件，将题目信息加载到内存中：
         * 先按行读取题目列表文件，获取题目编号后再拼接出保存单个题目信息的文件(desc.txt\header.cpp\tail.cpp)的路径，
         * 靠以上四个文件将每一题的题目信息加载到各自的Question结构体中，然后将 题号和结构体的键值对 保存到题库无序图中。
         ********************************************************************************************************/
        bool LoadQuestionList(const string &question_list)
        {
            // 1.按行读取questions.list文件中并从每行中切割出单个题目信息用来填充题目描述结构体Question
            // 1.1以读方式打开questions.list文件
            ifstream in(question_list);
            if (!in.is_open())         // 如果文件未被打开
            {
                LOG(FATAL) << " 加载题库失败,OJ系统无法继续运行,请检查是否存在题库文件：questions.list。\n";
                return false;
            }
            // 1.2按行读取questions.list文件
            string line;
            while (getline(in, line))
            {
                //1.3 切割字符串得到单个题目信息（要切割字符串示例："1 判断回文数 简单 1 30000"。）
                vector<string> tokens;                       // 保存questions.list文件中每行分割出的单个题目信息。
                StringUtil::SplitString(line, &tokens, " "); // 分割字符串。
                if (tokens.size() != 5)                      // 如果没有切分出五个部分，文件格式存在问题。
                {
                    LOG(WARNING) << "加载部分题目失败, 请检查questions.list文件中内容的格式。\n";
                    continue; // 跳过本行继续切割。
                }
                //1.4 填充题目描述结构体（目前要填充的内容示例："1 判断回文数 简单 1 30000"。）
                Question q;                            // 每个题目创建一个结构体来描述。
                q.number = tokens[0];                  // 题目编号
                q.title  = tokens[1];                  // 标题
                q.star   = tokens[2];                  // 难度
                q.cpu_limit = atoi(tokens[3].c_str()); // 时间限制
                q.mem_limit = atoi(tokens[4].c_str()); // 空间限制
            
            // 2. 将保存单个题目信息的文件(desc.txt\header.cpp\tail.cpp)中的题目信息加载到Question结构体中
                // 2.1 生成单个题目文件夹的路径："./questions/题目编号/"，用于找到保存单个题目信息文件夹
                string path = questins_path;
                path += q.number;
                path += "/";
                // 2.2 找到保存单个题目信息的文件(desc.txt\header.cpp\tail.cpp)并读取题目信息填充到Question结构体中。
                FileUtil::ReadFile(path + "desc.txt", &(q.desc), true);     //题目：题目描述
                FileUtil::ReadFile(path + "header.cpp", &(q.header), true); //预设代码
                FileUtil::ReadFile(path + "tail.cpp", &(q.tail), true);     //代码结尾

            // 3. 将题号和结构体的键值对保存到题库无序图中。
                questions.insert({q.number, q});
            }
            LOG(INFO) << "题库加载成功!\n";
            in.close(); //关闭输入文件流
            return true;
        }

        /* 将题库无序图中所有题目信息都提取出来，通过输出型参数返回。*/
        bool GetAllQuestions(vector<Question> *out)
        {
            if (questions.size() == 0)      // 如果题库无序图为空，用户获取题库失败
            {
                LOG(WARNING) << "题库为空，用户获取题库失败，请尽快处理。";
                return false;
            }
            for (const auto &q : questions) // 提取所有题目信息
            {
                out->push_back(q.second);   // first: key, second: value
            }

            return true;
        }

        /* 根据题号将题库无序图中指定的单个题目信息提取出来，通过输出型参数返回。*/
        bool GetOneQuestion(const std::string &number, Question *q)
        {
            const auto &iter = questions.find(number); // 查找单个题目是否在题库无序图中
            if (iter == questions.end())               // 如果没有找到，
            {
                LOG(WARNING) << "用户获取单个题目失败，题目编号: " << number << "\n";
                return false;
            }
            (*q) = iter->second; //提取单个题目的信息
            return true;
        }
    };
}